
LLVM自带一些sanitizer。这些Pass以某种方式检测中间表示(IR)，以检查应用程序的某些不当行为。通常，需要库支持，这是compiler-rt项目的一部分。可以在Clang中启用sanitizer，这让它们使用起来更加方便。下面的小节中，我们将介绍可用的sanitizer，即地址、内存和线程。我们先来看看地址sanitizer。\par

\hspace*{\fill} \par %插入空行
\textbf{用地址sanitizer检测内存访问问题}

可以使用地址sanitizer来检测应用程序中的两个内存访问错误。这包括一些常见的错误，比如：在释放动态分配的内存后使用它，或者在已分配内存的边界之外写入动态分配的内存。\par

当启用地址sanitizer时，地址sanitizer将用它自己的版本替换对malloc()和free()函数的调用，并使用检查保护程序检测所有内存访问。当然，这给应用程序增加了很多开销，您将只在应用程序的测试阶段使用地址消毒剂。如果对实现细节感兴趣，可以在llvm/lib/Transforms/Instrumentation/\allowbreak AddressSanitzer.cpp文件中找到Pass源，并在\url{https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm}中找到算法描述。\par

让我们运行一个简短的示例来演示地址sanitizer的功能。下面的示例应用程序outfbounds.c分配了12字节的内存，但初始化了14字节:\par

\begin{lstlisting}[caption={}]
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
	char *p = malloc(12);
	memset(p, 0, 14);
	return (int)*p;
}
\end{lstlisting}

您可以编译并运行此应用程序，而不会注意到任何问题。这是这中错误的典型特征。即使在较大的应用程序中，这种错误也可能在很长一段时间内不被注意。但是，如果使用-fsanitize=address选项启用地址sanitizer，那么应用程序在检测到错误后将停止。\par

使用-g选项启用调试符号也很有用，因为它有助于识别源文件中错误的位置。下面的代码示例说明了如何在启用地址消毒剂和调试符号的情况下编译源文件:\par

\begin{tcolorbox}[colback=white,colframe=black]
\$ clang -fsanitize=address -g outofbounds.c -o outofbounds
\end{tcolorbox}

现在，当运行应用程序时，会得到一个冗长的错误报告:\par

\begin{tcolorbox}[colback=white,colframe=black]
\$ ./outofbounds \\
============================================= \\
=== \\
==1067==ERROR: AddressSanitizer: heap-buffer-overflow on \\
address 0x60200000001c at pc 0x00000023a6ef bp 0x7fffffffeb10 \\
sp 0x7fffffffe2d8 \\
WRITE of size 14 at 0x60200000001c thread T0 \\
\hspace*{1cm}\#0 0x23a6ee in \underline{~~}asan\underline{~}memset /usr/src/contrib/llvm-project/ \\
compiler-rt/lib/asan/asan\underline{~}interceptors\underline{~}memintrinsics.cpp:26:3 \\
\hspace*{1cm}\#1 0x2b2a03 in main /home/kai/sanitizers/outofbounds.c:6:3 \\
\hspace*{1cm}\#2 0x23331f in \underline{~}start /usr/src/lib/csu/amd64/crt1.c:76:7
\end{tcolorbox}

报告还包含关于内存内容的详细信息。重要的信息是错误的类型(在本例中是堆缓冲区溢出)和出错的源行。要找到源码行，可以查看位置\#1的堆栈跟踪，这是地址sanitizer拦截应用程序执行之前的最后一个位置。它显示了outfbounds.c文件中的第6行，这一行包含了对memset()的调用——实际上，这就是发生内存溢出的位置。\par

替换包含memset(p, 0, 14);在outfbounds.c文件中使用以下代码，然后在释放内存后引入对内存的访问。并将源代码存储在useafterfree.c中:\par

\begin{lstlisting}[caption={}]
memset(p, 0, 12);
free(p);
\end{lstlisting}

同样，如果编译并运行，会检测到内存释放后指针的使用情况:\par

\begin{tcolorbox}[colback=white,colframe=black]
\$ clang -fsanitize=address -g useafterfree.c -o useafterfree \\
\$ ./useafterfree \\
============================================== \\
=== \\
==1118==ERROR: AddressSanitizer: heap-use-after-free on address \\
0x602000000010 at pc 0x0000002b2a5c bp 0x7fffffffeb00 sp \\
0x7fffffffeaf8 \\
READ of size 1 at 0x602000000010 thread T0 \\
\hspace*{1cm}\#0 0x2b2a5b in main /home/kai/sanitizers/ \\
useafterfree.c:8:15 \\
\hspace*{1cm}\#1 0x23331f in \underline{~}start  /usr/src/lib/csu/amd64/crt1.c:76:7
\end{tcolorbox}

这一次，报告指向第8行，其中包含p指针的释放。\par

在x86\underline{~}64 Linux和macOS上，也可以启用泄漏检测器。如果在运行应用程序之前将ASAN\underline{~}OPTIONS环境变量设置为detect\underline{~}leaks=1，还会得到一个关于内存泄漏的报告。可以这样做:\par

\begin{tcolorbox}[colback=white,colframe=black]
\$ ASAN\underline{~}OPTIONS=detect\underline{~}leaks=1 ./useafterfree
\end{tcolorbox}

地址sanitizer非常有用，因为它捕获了一类用其他方法很难检测到的bug。内存sanitizer执行类似的任务，我们将在下一节中研究它。\par

\hspace*{\fill} \par %插入空行
\textbf{使用内存sanitizer查找未初始化的内存访问}

使用未初始化的内存是另一类难以发现的错误。在C和C++中，一般的内存分配例程不会用默认值初始化内存缓冲区，对于堆栈上的变量也是如此。\par

出现错误的机会很多，而内存sanitizer有助于找到错误。如果对实现细节感兴趣，可以在llvm/lib/Transforms/Instrumentation/MemorySanitizer.cpp中找到内存sanitizer Pass的源文件。文件顶部的注释解释了实现思想。\par

让我们运行一个小示例，并将下面的源代码保存为memory.c文件。你应该注意到x变量没有初始化，而是用作返回值:\par

\begin{lstlisting}[caption={}]
int main(int argc, char *argv[]) {
	int x;
	return x;
}
\end{lstlisting}

没有sanitizer，应用程序将运行得很好。如果你使用-fsanitize=memory选项，就会得到一个错误报告:\par

\begin{tcolorbox}[colback=white,colframe=black]
\$ clang -fsanitize=memory -g memory.c -o memory \\
\$ ./memory \\
==1206==WARNING: MemorySanitizer: use-of-uninitialized-value \\
\hspace*{1cm}\#0 0x10a8f49 in main /home/kai/sanitizers/memory.c:3:3 \\
\hspace*{1cm}\#1 0x1053481 in \underline{~}start /usr/src/lib/csu/amd64/crt1.c:76:7 \\
\\
SUMMARY: MemorySanitizer: use-of-uninitialized-value /home/kai/ \\
sanitizers/memory.c:3:3 in main \\
Exiting
\end{tcolorbox}

与地址sanitizer一样，内存sanitizer在发现第一个错误时停止应用程序。\par

下一节中，我们将了解如何使用线程sanitizer来检测多线程应用程序中的数据竞争。\par

\hspace*{\fill} \par %插入空行
\textbf{用线程sanitizer指出数据竞争}

为了充分利用现代CPU的功能，应用程序现在使用多线程。这是一项强大的技术，但它也引入了新的错误来源。多线程应用程序中一个常见的问题是，对全局数据的访问没有保护，例如：没有使用互斥锁或信号量。这样的问题称为数据竞争。线程sanitizer可以检测基于pthread的应用程序和使用LLVM libc++实现的应用程序中的数据竞争。可以在llvm/lib/Transforms/Instrumentation/\allowbreak ThreadSanitize.cpp文件中找到实现。\par

为了演示线程sanitizer的功能，我们将创建一个简单的生产者/消费者的应用程序。生产者线程增加一个全局变量，而消费者线程减少同一个变量。对全局变量的访问不受保护，因此这显然是一场数据竞争。在thread.c文件中保存以下源代码:\par

\begin{lstlisting}[caption={}]
#include <pthread.h>

int data = 0;

void *producer(void *x) {
	for (int i = 0; i < 10000; ++i) ++data;
	return x;
}

void *consumer(void *x) {
	for (int i = 0; i < 10000; ++i) --data;
	return x;
}

int main() {
	pthread_t t1, t2;
	pthread_create(&t1, NULL, producer, NULL);
	pthread_create(&t2, NULL, consumer, NULL);
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	return data;
}
\end{lstlisting}

前面的代码中，数据变量在两个线程之间共享。这里，它是int类型，以使示例简单。最常见的情况是使用std::vector类或类似的数据结构。这两个线程运行producer()和consumer()函数。\par

producer()函数只增加数据变量，而consumer()函数减少数据变量。没有实现访问保护，因此这构成了一场数据竞争。main()函数使用pthread\underline{~}create()函数启动两个线程，使用pthread\underline{~}join()函数等待线程结束，并返回数据变量的当前值。\par

如果编译并运行此应用程序，则不会注意到任何错误，返回值总是0。如果执行的循环次数增加100倍，则会出现一个错误。本例中，返回值不等于0，您将看到显示其他值。\par

您可以使用线程sanitizer来标识数据竞争。要在启用了线程sanitizer的情况下进行编译，需要将-fsanitize=thread选项传递给Clang。使用-g选项添加调试符号可以在报告中提供行号，这很有帮助。注意，还需要链接pthread库:\par

\begin{tcolorbox}[colback=white,colframe=black]
\$ clang -fsanitize=thread -g thread.c -o thread -lpthread \\
\$ ./thread \\
================== \\
WARNING: ThreadSanitizer: data race (pid=1474) \\
\hspace*{0.5cm}Write of size 4 at 0x000000cdf8f8 by thread T2: \\
\hspace*{1cm}\#0 consumer /home/kai/sanitizers/thread.c:11:35 
(thread+0x2b0fb2) \\
\\
\hspace*{0.5cm}Previous write of size 4 at 0x000000cdf8f8 by thread T1: \\
\hspace*{1cm}\#0 producer /home/kai/sanitizers/thread.c:6:35 
(thread+0x2b0f22)
\\
\hspace*{0.5cm}Location is global 'data' of size 4 at 0x000000cdf8f8 
(thread+0x000000cdf8f8)
\\
\hspace*{0.5cm}Thread T2 (tid=100437, running) created by main thread at: \\
\hspace*{1cm}\#0 pthread\underline{~}create /usr/src/contrib/llvm-project/ \\
compiler-rt/lib/tsan/rtl/tsan\underline{~}interceptors\underline{~}posix.cpp:962:3 
(thread+0x271703) \\
\hspace*{1cm}\#1 main /home/kai/sanitizers/thread.c:18:3 
(thread+0x2b1040) \\
\\
\hspace*{0.5cm}Thread T1 (tid=100436, finished) created by main  thread at: \\
\hspace*{1cm}\#0 pthread\underline{~}create  /usr/src/contrib/llvm-project/ \\
compiler-rt/lib/tsan/rtl/tsan\underline{~}interceptors\underline{~}posix.cpp:962:3 
(thread+0x271703) \\
\\
\hspace*{1cm}\#1 main /home/kai/sanitizers/thread.c:17:3 
(thread+0x2b1021) \\
\\
SUMMARY: ThreadSanitizer: data race /home/kai/sanitizers/ 
thread.c:11:35 in consumer \\
================== \\
ThreadSanitizer: reported 1 warnings
\end{tcolorbox}

报告指向源文件的第6行和第11行，其中访问全局变量。它还显示了两个名为T1和T2的线程访问了该变量，以及分别调用pthread\underline{~}create()函数的文件和行号。\par

本节中，我们学习了如何使用三种sanitizer来查找应用程序中的常见问题。地址sanitizer帮助我们识别常见的内存访问错误，例如：越界访问或在释放后使用内存。使用内存sanitizer，可以找到对未初始化内存的访问，线程sanitizer帮助我们查找数据竞争。\par

下一节中，我们将尝试通过在随机数据上运行应用程序(称为模糊测试)来触发sanitizer。\par




















